﻿using System;
using System.Collections.Generic;
using System.Text;

namespace ircCommunication
{

    // ircMessage - 512 ASCII characters maximum length
    // 510 characters + CR + LF = 512
    
    // Pseudo BNF representation

    //<message>  ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
    //<prefix>   ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
    //<command>  ::= <letter> { <letter> } | <number> <number> <number>
    //<SPACE>    ::= ' ' { ' ' }
    //<params>   ::= <SPACE> [ ':' <trailing> | <middle> <params> ]

    //<middle>   ::= <Any *non-empty* sequence of octets not including SPACE
    //           or NUL or CR or LF, the first of which may not be ':'>
    //<trailing> ::= <Any, possibly *empty*, sequence of octets not including
    //             NUL or CR or LF>

    //<crlf>     ::= CR LF

    public enum iMsgType : uint
    {
        PASS,
        NICK,
        USER,
        SERVER,
        OPER,
        QUIT,
        SQUIT,
        JOIN,
        PART,
        MODE,
        TOPIC,
        NAMES,
        LIST,
        INVITE,
        KICK,
        VERSION,
        STATS,
        LINKS,
        TIME,
        CONNECT,
        TRACE,
        ADMIN,
        INFO,
        PRIVMSG,
        NOTICE,
        WHO,
        WHOIS,
        WHOWAS,
        KILL,
        PING,
        PONG,
        ERROR,
        //* Not required message types
        AWAY,
        REHASH,
        RESTART,
        SUMMON,
        USERS,
        WALLOPS,
        USERHOST,
        ISON,
        //**************************/
        IRCMSG_ERROR,
        NUMERIC_REPLY        
    }
    /// <summary>
    /// The IRC message class. Contains the parsed IRC message.
    /// </summary>
    public class ircMessage
    {
        static protected String[] dOutFormat = new String[42] {"PASS message (server only).",
                                                            "NICK message (server only).",
                                                            "USER message (server only).",
                                                            "SERVER message (server only).",
                                                            "OPER message (server only).",
                                                            "{N} has quit ({PL}).",
                                                            "Received SQUIT message.",
                                                            "{N} has joined the channel.",
                                                            "{N} has left the channel ({PL}).",
                                                            "MODE message (server only).",
                                                            "TOPIC message (server only).",
                                                            "NAMES message (server only).",
                                                            "LIST message (server only).",
                                                            "INVITE message (server only).",
                                                            "KICK message (server only).",
                                                            "VERSION message (server only).",
                                                            "STATS message (server only).",
                                                            "LINK message (server only).",
                                                            "TIME message (server only).",
                                                            "CONNECT message (server only).",
                                                            "TRACE message (server only).",
                                                            "ADMIN message (server only).",
                                                            "INFO message (server only).",
                                                            "<{N}> {PL}",
                                                            "-Notice from {N}- {PL}",
                                                            "WHO message (server only).",
                                                            "WHOIS message (server only).",
                                                            "WHOWAS message (server only).",
                                                            "{N} killed {P0}. Reason: {PL}",
                                                            "PING message received ({P0}; {P1})",
                                                            "PONG message received ({P0}; {P1})",
                                                            "ERROR! {PL}",
                                                            "{N} is now away ({PL})",
                                                            "REHASH message (server only).",
                                                            "RESTART message (server only).",
                                                            "SUMMON message (server only).",
                                                            "USERS message (server only).",
                                                            "WALLOPS message (server only).",
                                                            "USERHOST message (server only).",
                                                            "ISON message (server only).",
                                                            "Error while parsing message. (Class ircMessage).",
                                                            "{N} - {C}: {PL}"
                                                            };
        
        public String rawMessage { get; private set; }

        //** PREFIX
        // does the message have a prefix?
        public bool hasPrefix { get; private set; }
        // contains the whole prefix (not didived into parts)
        public String prefix { get; private set; }
        // contains the server name/nick name
        public String pName { get; private set; }
        // does the prefix have user specified?
        public bool pHasUser { get; private set; }
        // the user specified in prefix
        public String pUser { get; private set; }
        // does the prefix have host specified?
        public bool pHasHost { get; private set; }
        // the host specified in prefix
        public String pHost { get; private set; }

        //** COMMAND
        // the string of the command
        public String command { get; private set; }
        // type of the message based on the command
        public iMsgType type { get; private set; }
        // the number of the message (between 1 and 999): valid only if the type is NUMERIC
        public int cNumber { get; private set; }

        //** PARAMETERS
        // the string array containing all parameters
        public String[] parameters { get; private set; }
        // number of parameters (0 - 15)
        public int prmCount { get; private set; }

        // setup needed variables
        /// <summary>
        /// Initializes a new instance of the <see cref="ircMessage"/> class.
        /// </summary>
        public ircMessage()
        {
            rawMessage = "300 :unused\r\n";
            parseRawMessage();
        }

        // constructor of message from raw input
        /// <summary>
        /// Initializes a new instance of the <see cref="ircMessage"/> class.
        /// </summary>
        /// <param name="rawInput">The raw input.</param>
        public ircMessage(String rawInput)
        {

            cropRawInput(rawInput);
            parseRawMessage();
                    
        }

        // fully parses a string into IRC message
        /// <summary>
        /// Creates the specified raw input.
        /// </summary>
        /// <param name="rawInput">The raw input.</param>
        /// <returns></returns>
        public bool Create(String rawInput)
        {
            
            cropRawInput(rawInput);
            parseRawMessage();

            if (type == iMsgType.IRCMSG_ERROR) return false; else return true;

        }

        /// <summary>
        /// Creates the priv MSG.
        /// </summary>
        /// <param name="targets">The targets.</param>
        /// <param name="text">The text.</param>
        /// <returns></returns>
        public bool CreatePrivMsg(String[] targets, String text)
        {
            StringBuilder sb = new StringBuilder("PRIVMSG ");

            for (int i = 0; i < targets.GetLength(0); i++)
            {
                if (i != 0) sb.Append(",");
                sb.Append(targets[i]);
                sb.Append(" ");
            }

            sb.Append(":");
            sb.Append(text);
            sb.Append("\r\n");

            return this.Create(sb.ToString());
        }

        /// <summary>
        /// Creates the priv MSG.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="text">The text.</param>
        /// <returns></returns>
        public bool CreatePrivMsg(String target, String text)
        {

            if (target == null || text == null) throw new NullReferenceException("target or text cannot be null.");
            
            StringBuilder sb = new StringBuilder("PRIVMSG ");

            sb.Append(target);

            sb.Append(" :");
            sb.Append(text);
            sb.Append("\r\n");

            return this.Create(sb.ToString());
        }

        /// <summary>
        /// Replies to ping.
        /// </summary>
        /// <returns></returns>
        public bool ReplyToPing()
        {

            //if (this.type != iMsgType.PING) return false;

            this.command = this.command.Replace("PING", "PONG");
            this.type = iMsgType.PONG;
            this.rawMessage = this.rawMessage.Replace("PING", "PONG");

            return true;

        }

        /// <summary>
        /// Replies to ping.
        /// </summary>
        /// <param name="rawPingMsg">The raw ping MSG.</param>
        /// <returns></returns>
        public bool ReplyToPing(String rawPingMsg)
        {

            if (this.Create(rawPingMsg)) return this.ReplyToPing();

            return false;

        }

        // generates a formated output string of the message
        /// <summary>
        /// Outputs the specified format.
        /// </summary>
        /// <param name="format">The format.</param>
        /// <returns>The resulting text</returns>
        public String Output(String format)
        {

            if (format == null) return null;

            // available tokens:
            // if any of these tokens is null it is added as ""
            // {N} name
            // {U} user
            // {H} host
            // {C} command
            // {PL} last parameter 
            // non-existent parameters are not replaced (remain in the format string)
            // {PX} where X = 0-14 (hexa numbers 0 - 14) substitutes for parameters[X]
            
            StringBuilder sb = new StringBuilder(format);

            if (format.Contains("{N}")) sb.Replace("{N}", this.pName);
            if (format.Contains("{U}")) sb.Replace("{U}", this.pUser);
            if (format.Contains("{H}")) sb.Replace("{H}", this.pHost);
            if (format.Contains("{C}")) sb.Replace("{C}", this.command);
            if (format.Contains("{PL}")) sb.Replace("{PL}", this.parameters[prmCount - 1]);

            // params
            String r = "{P0}";
            for (int i = 0; i < this.prmCount; i++)
            {
                if (format.Contains(r)) sb.Replace(r, this.parameters[i]);
                // TODO - add hexa numbers!
                r.Replace(i.ToString(), (i + 1).ToString());
            }

            return sb.ToString();

        }

        // generates a formated output string of the message
        /// <summary>
        /// Outputs the default format.
        /// </summary>
        /// <returns>The resulting text.</returns>
        public String Output()
        {

            String source = dOutFormat[System.Convert.ToUInt32(type)];
            StringBuilder sb = new StringBuilder(source);

            if (source.Contains("{N}")) sb.Replace("{N}", this.pName);
            if (source.Contains("{U}")) sb.Replace("{U}", this.pUser);
            if (source.Contains("{H}")) sb.Replace("{H}", this.pHost);
            if (source.Contains("{C}")) sb.Replace("{C}", this.command);
            if (source.Contains("{PL}")) sb.Replace("{PL}", this.parameters[prmCount - 1]);

            // params
            String r = "{P0}";
            for (int i = 0; i < this.prmCount; i++)
            {
                if (source.Contains(r)) sb.Replace(r, this.parameters[i]);

                r.Replace(i.ToString(), (i + 1).ToString());
            }

            return sb.ToString();

        }

        // makes sure that the variable rawMessage contains
        // exactly one IRC message (not parsed)
        /// <summary>
        /// Crops the raw input.
        /// </summary>
        /// <param name="rawInput">The raw input.</param>
        protected void cropRawInput(String rawInput)
        {
            
            String[] temp = new String[2];
            string[] delim = new string[] { "\r\n" };

            if (rawInput != null)
            {

                if (rawInput.Length > 512)
                {
                    temp = rawInput.Remove(512).Split(delim, 2, StringSplitOptions.RemoveEmptyEntries);
                }
                else
                {
                    temp = rawInput.Split(delim, 2, StringSplitOptions.RemoveEmptyEntries);
                }

                rawMessage = temp[0];

                //if (!rawMessage.Contains("\r\n"))
                //{
                //    rawMessage += "\r\n"; 
                //}

            }
            else
            {
                rawMessage = "300 :unused\r\n";
            }
        
        }

        /// <summary>
        /// Parses the raw message.
        /// </summary>
        protected void parseRawMessage()
        {

            String[] temp;
            string[] delim = new string[] { " " };

            temp = rawMessage.Split(delim, 2, StringSplitOptions.RemoveEmptyEntries);

            if (rawMessage.IndexOf(':') == 0)
            {

                // message has a prefix
                hasPrefix = true;
                
                // split into prefix - command - parameters
                temp = rawMessage.Split(delim, 3, StringSplitOptions.RemoveEmptyEntries);
                
                if (temp.GetLength(0) == 3)
                {
                    // we have params
                    prefix = temp[0];
                    command = temp[1];
                    prmCount = splitParameters(temp[2]);

                }
                else
                {
                    // no params
                    if (temp.GetLength(0) == 1) { this.type = iMsgType.IRCMSG_ERROR; return; } // error!

                    prefix = temp[0];
                    command = temp[1];
                    prmCount = 0;

                }

                pName = getNameFromPrefix(prefix);
                pUser = getUserFromPrefix(prefix);
                if (pUser == null) pHasUser = false; else pHasUser = true;
                pHost = getHostFromPrefix(prefix);
                if (pUser == null) pHasHost = false; else pHasHost = true;

            }
            else
            {

                // no prefix in the message
                hasPrefix = false;
                pHasUser = false;
                pHasHost = false;

                // split into command - parameters
                temp = rawMessage.Split(delim, 2, StringSplitOptions.RemoveEmptyEntries);

                if (temp.GetLength(0) == 1)
                {
                    command = temp[0];
                }
                else
                {
                    command = temp[0];
                    prmCount = splitParameters(temp[1]);
                }

            }

            type = getType(command);

        }

        // splits the parameter section into separate parameters
        // removes the ':' from the last paramater (trailing)
        // returns the total number of parameters
        /// <summary>
        /// Splits the parameters.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <returns></returns>
        protected int splitParameters(String source)
        {

            // no params
            if (source == null) return 0;

            // just one parameter
            if (source.StartsWith(":"))
            {

                parameters = new String[1];
                parameters[0] = source.Remove(0, 1);
                return 1;

            }

            // at least two parameters
            int paramCount = 0;
            int trailing = -1;
            String temp;
            String[] delim = new String[]{" "};
            String[] prms;
            String lastPrm;
            
            // determine if we have a trailing parameter
            trailing = source.IndexOf(" :");
            
            if (trailing != -1)
            {
                // we have a trailing parameter

                // copy all text before the " :" into temp
                // split temp into parameters
                temp = source.Substring(0, source.IndexOf(" :"));
                prms = temp.Split(delim, 14, StringSplitOptions.RemoveEmptyEntries);

                // copy the last parameter into lastPrm
                lastPrm = source.Substring(source.IndexOf(" :") + 1);

                paramCount = prms.GetLength(0) + 1;
                parameters = new String[paramCount];

                // copy all parameters into the parameters array
                for (int i = 0; i < paramCount - 1; i++)
                {
                    parameters[i] = prms[i];
                }

                // dont forget the last one and remove the ":"
                parameters[paramCount - 1] = lastPrm.Remove(0, 1);

            }
            else
            {
                // no trailing parameter
                prms = source.Split(delim, 15, StringSplitOptions.RemoveEmptyEntries);

                paramCount = prms.GetLength(0);
                parameters = new String[paramCount];

                // copy all parameters into the parameters array
                for (int i = 0; i < paramCount; i++)
                {
                    parameters[i] = prms[i];
                }

            }

            // return the number of parameters
            return paramCount;

        }

        /// <summary>
        /// Gets the name from prefix.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <returns></returns>
        protected String getNameFromPrefix(String source)
        {

            int exclamation = source.IndexOf('!');
            int funnyA = source.IndexOf('@');

            // no user or host
            if (exclamation == -1 && funnyA == -1) return source.Substring(1);

            // no user, host present
            // ":name@host"
            if (exclamation == -1)
            {
                if (funnyA < 2) return null;

                return source.Substring(1, funnyA - 1);

            }

            // user present, no host
            // ":name!user"
            if (exclamation < 2) return null;

            return source.Substring(1, exclamation - 1);

        }

        /// <summary>
        /// Gets the user from prefix.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <returns></returns>
        protected String getUserFromPrefix(String source)
        {

            int exclamation = source.IndexOf('!');
            int funnyA = source.IndexOf('@');

            if (exclamation == -1) return null;
            else
            {
                if (funnyA == -1) return source.Substring(exclamation + 1);
                else return source.Substring(exclamation + 1, (funnyA - exclamation) - 1);
            }

        }
        //<prefix>   ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
        /// <summary>
        /// Gets the host from prefix.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <returns></returns>
        protected String getHostFromPrefix(String source)
        {

            int funnyA = source.IndexOf('@');

            if (funnyA == -1) return null;
            else return source.Substring(funnyA + 1);

        }

        /// <summary>
        /// Gets the type.
        /// </summary>
        /// <param name="cmd">The CMD.</param>
        /// <returns></returns>
        protected iMsgType getType(String cmd)
        {

            cNumber = -1;
            // determine message type
            if (cmd.Equals("PASS")) return iMsgType.PASS;
            if (cmd.Equals("NICK")) return iMsgType.NICK;
            if (cmd.Equals("USER")) return iMsgType.USER;
            if (cmd.Equals("SERVER")) return iMsgType.SERVER;
            if (cmd.Equals("OPER")) return iMsgType.OPER;
            if (cmd.Equals("QUIT")) return iMsgType.QUIT;
            if (cmd.Equals("SQUIT")) return iMsgType.SQUIT;
            if (cmd.Equals("JOIN")) return iMsgType.JOIN;
            if (cmd.Equals("PART")) return iMsgType.PART;
            if (cmd.Equals("MODE")) return iMsgType.MODE;
            if (cmd.Equals("TOPIC")) return iMsgType.TOPIC;
            if (cmd.Equals("NAMES")) return iMsgType.NAMES;
            if (cmd.Equals("NICK")) return iMsgType.NICK;
            if (cmd.Equals("LIST")) return iMsgType.LIST;
            if (cmd.Equals("INVITE")) return iMsgType.INVITE;
            if (cmd.Equals("KICK")) return iMsgType.KICK;
            if (cmd.Equals("VERSION")) return iMsgType.VERSION;
            if (cmd.Equals("STATS")) return iMsgType.STATS;
            if (cmd.Equals("LINKS")) return iMsgType.LINKS;
            if (cmd.Equals("TIME")) return iMsgType.TIME;
            if (cmd.Equals("CONNECT")) return iMsgType.CONNECT;
            if (cmd.Equals("TRACE")) return iMsgType.TRACE;
            if (cmd.Equals("ADMIN")) return iMsgType.ADMIN;
            if (cmd.Equals("PRIVMSG")) return iMsgType.PRIVMSG;
            if (cmd.Equals("NOTICE")) return iMsgType.NOTICE;
            if (cmd.Equals("WHO")) return iMsgType.WHO;
            if (cmd.Equals("WHOIS")) return iMsgType.WHOIS;
            if (cmd.Equals("WHOWAS")) return iMsgType.WHOWAS;
            if (cmd.Equals("KILL")) return iMsgType.KILL;
            if (cmd.Equals("PING")) return iMsgType.PING;
            if (cmd.Equals("PONG")) return iMsgType.PONG;
            if (cmd.Equals("ERROR")) return iMsgType.ERROR;
            //* Not required message types
            if (cmd.Equals("AWAY")) return iMsgType.AWAY;
            if (cmd.Equals("REHASH")) return iMsgType.REHASH;
            if (cmd.Equals("RESTART")) return iMsgType.RESTART;
            if (cmd.Equals("SUMMON")) return iMsgType.SUMMON;
            if (cmd.Equals("USERS")) return iMsgType.USERS;
            if (cmd.Equals("WALLOPS")) return iMsgType.WALLOPS;
            if (cmd.Equals("USERHOST")) return iMsgType.USERHOST;
            if (cmd.Equals("ISON")) return iMsgType.ISON;
            //**************************/

            // no known message type
            // is it numeric?
            try
            {
                int num = System.Convert.ToInt32(cmd);

                if (num <= 0)
                {
                    return iMsgType.IRCMSG_ERROR;
                }
                else
                {
                    cNumber = num;
                    return iMsgType.NUMERIC_REPLY;
                }
            }
            catch (FormatException)
            {
                return iMsgType.IRCMSG_ERROR;
            }

        }

    }

}
